home *** CD-ROM | disk | FTP | other *** search
Wrap
/*---------------------------------------------------------------------------------------- Lurkers Greg Anderson January 1994 AppleLink: G.ANDERSON Internet: greggor@apple.com About Lurkers: Lurkers searches a directory for files that are modifiable, including files not in a project, files checked out for modification, and modify-read-only files. Search modes (pick any one): -modifiable (default) Search for files not in a project, files checked out for modification, and modify-read-only files -notmodifiable Search for files checked into a project that are not modifiable (has 'ckid', but is neither MRO nor checked out for modification) -mro Search for files that are checked into a project (has 'ckid'), but have been modified-read-only. -insomeproject Search for files with a 'ckid' resource--modifiable or not -notinanyproject Search for files without a 'ckid' resource -modifiableinproject Search for files with a 'ckid' resource that are MRO'ed or checked out for modification (does not actually check project database to see if any changes have been made, though) -checkedoutmodifiable Searches for files with a 'ckid' resource that are checked out for modification. Other flags: -r Search specified directory recursively -s Short output (only list names of files that match the search criterion) -f Output full pathnames -q Don't quote filenames with special characters (Quoting only done in short output) -rev Show the revision number of the file -textonly Ignore files whose type is not TEXT -ignore n Ignore files and folders whose name is 'n' Revision History: 1.0, 19 Jan 94 Initial release 1.1, 20 Jan 94 Lurkers being too strict in its test for checked out files, so it missed some. Also, I failed to check for errors when opening the resource fork of a file (oops!), so files without a resource fork reported the information applicable to the next item in the resource chain (not a problem unless you check your MPW shell into a project--but our team does!) 1.2, 25 Jan 94 Lurkers still missing files; make its test even less strict (pretty sure I got it right this time) 1.3, 27 Jan 94 Added flags -f -q -rev -textonly 1.4, 16 Feb 94 Added partial pathnames, -checkedoutmodifiable, -mro 1.5, 2 Jun 94 Andy wanted me to remove some spurrious blank lines in -s mode 1.6, 9 Jun 94 Improved help, added -ignore, allowed single files to be tested To Do: Quoted files are not handled correctly; for example, a file that has a single-quote in it needs to be escaped. The current output is: ''filename'' The desired output is: ''∂''filename'∂''' It would also be nice if -ignore handled wildcards, or even regular expresions ----------------------------------------------------------------------------------------*/ #include <Types.h> #include <ctype.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #include <StdLib.h> #include <ErrMgr.h> #include <CursorCtl.h> #include <Errors.h> #include <QuickDraw.h> #include <SysEqu.h> #include <Files.h> #include <Memory.h> #include <Resources.h> #include <CursorCtl.h> // // First line of usage string (more than this is printed out) // static char* usage = "%s [file's projector status] [option…] directory|file…\n"; static long optionsSpecified = false; // // We know a little bit about the contents of a ckid resource: // struct ckidheader { long fUnknownID; // Different after every projector operation long fUnkownConstant; // Always the same short fUnknown1; // Usually 0004 short fIsCheckedOut; // Usually 2, 3 or 4 if checked out, always 0 if not short fIsMRO; // 0001 if MRO, 0000 if not short fUnknown2; // Usually 0000 long fUnknown3; // I don't know or care what these are long fUnknown4; long fUnknown5; long fUnknown6; long fUnknown7; long fUnknown8; unsigned char fProjectName; // null-terminated pascal string // After the project name is the name of the // person who checked out this file on this // machine (no relation to the person who currently // has the file checked out). Also null-terminated // After the user name is the revision number // of the file, in ascii and null-terminated. }; // // Projector states that we know about: // (One and only one of these bits may be set at any time, as we switch on them) // #define kNotModifiable 1 #define kModifiedReadOnly 2 #define kCheckedOut 4 #define kNotInAProject 8 // // This mask must include all of the file project states above // #define kFileProjectMask (kNotModifiable | kModifiedReadOnly | kCheckedOut | kNotInAProject) // // Here are some extra file project states that we do not switch on // (so these bits may be set in addition to the ones above) // #define kHasCKIDAndIsModifiable 0x10 #define kHasCKIDAndIsCheckedOut 0x20 // // Global options, because there is no need to pass it around // long gOptions = 0; long gWantOutputForState = kModifiedReadOnly | kCheckedOut | kNotInAProject; #define kShortOutput 1 #define kRecursive 2 #define kOutputRevision 4 #define kTextOnly 8 #define kFullPathnames 0x10 #define kDontQuote 0x20 #define kMagicUnusedDirID 0x8000000 // // Some global variables that should be local, but I don't // want my stack to get too large, and I don't feel like // being clever. // // The code is very carefully written to avoid using these // variables after they have been reused in a recursive call. // This is EVIL, but we want to avoid having a large stack. // CInfoPBRec pb; Str255 gFilename; Str255 gFolderpath; Str255 gTempProjectName; Str31 gTempRevisionNumber; char gTempRevisionString[64]; char** gIgnoreList; short gNumberOfIgnoreItems = 0; #define ConvertToUppercase(c) (((c >= 'a') && (c <= 'z')) ? c - 'a' + 'A' : c) //---------------------------------------------------------------------------------------- // OptionSpecified: //---------------------------------------------------------------------------------------- Boolean OptionSpecified(long flag) { return ((gOptions & flag) != 0); } // OptionSpecified //---------------------------------------------------------------------------------------- // ItemMatches // // It would be nice if this checked for wildcards... //---------------------------------------------------------------------------------------- Boolean ItemMatches(Str255 testItem, char* compareWith) { Boolean itemMatches = true; short i; for(i=1; i<=testItem[0]; ++i) { unsigned char c1 = testItem[i]; unsigned char c2 = *compareWith++; if(ConvertToUppercase(c1) != ConvertToUppercase(c2)) { itemMatches = false; break; } } // // If we ran out of characters in 'testItem', but there // are still characters left in 'compareWith', then the // two items don't really match // if(*compareWith != 0) itemMatches = false; return itemMatches; } // ItemMatches //---------------------------------------------------------------------------------------- // ItemIsInIgnoreList // // Return 'true' if the specified string is in the ignore list //---------------------------------------------------------------------------------------- Boolean ItemIsInIgnoreList(Str255 testItem) { Boolean shouldIgnore = false; short i; for(i=0; i< gNumberOfIgnoreItems; ++i) { if(ItemMatches(testItem, gIgnoreList[i])) { shouldIgnore = true; break; } } return shouldIgnore; } // ItemIsInIgnoreList //---------------------------------------------------------------------------------------- // BuildFolderPathname: //---------------------------------------------------------------------------------------- void BuildFolderPathname(short vRefNum, long dirID, Str255 folderpath, long stopDirID, Boolean firstTime) { Str63 thisName; OSErr err = noErr; thisName[0] = 0; folderpath[0] = 0; pb.dirInfo.ioCompletion = nil; pb.dirInfo.ioNamePtr = thisName; pb.dirInfo.ioResult = noErr; pb.dirInfo.ioVRefNum = vRefNum; pb.dirInfo.ioDrDirID = dirID; pb.dirInfo.ioFDirIndex = -1; err = PBGetCatInfo(&pb,false); if(err == noErr) { if(dirID != stopDirID) { BuildFolderPathname(vRefNum, pb.dirInfo.ioDrParID, folderpath, stopDirID, false); // // Append 'thisName' onto folderpath // thisName[thisName[0] + 1] = 0; strcpy(folderpath + folderpath[0] + 1, thisName + 1); folderpath[0] += thisName[0]; // // Append a colon onto folderpath // folderpath[folderpath[0] + 1] = ':'; ++folderpath[0]; } else if(!firstTime) { folderpath[1] = ':'; folderpath[0] = 1; } } } // BuildFolderPathname //---------------------------------------------------------------------------------------- // HasSpecialCharacters: //---------------------------------------------------------------------------------------- Boolean HasSpecialCharacters(Str255 pstring) { Boolean hasSpecial = false; short i; for(i=1;i<= pstring[0]; ++i) { switch(pstring[i]) { case ' ': case '\"': case '\∂': hasSpecial = true; break; } } return hasSpecial; } // HasSpecialCharacters //---------------------------------------------------------------------------------------- // ExamineProjectorInformation: //---------------------------------------------------------------------------------------- long ExamineProjectorInformation(short vRefNum, long dirID, const Str255 fileName, Str255 projectName, Str31 revisionNumber) { Handle ckidHandle = nil; short resRefNum; long projectorInfo = kNotInAProject; projectName[0] = 0; revisionNumber[0] = 0; resRefNum = HOpenResFile(vRefNum, dirID, fileName, fsRdPerm); if(resRefNum != -1) { ckidHandle = Get1Resource('ckid', 128); if(ckidHandle != nil) { struct ckidheader* ckpeek = nil; unsigned char* p; HLock(ckidHandle); ckpeek = *( (struct ckidheader**)ckidHandle); // // Look at a bit of the ckid resource // if(ckpeek->fIsCheckedOut != 0) projectorInfo = kCheckedOut; else if((ckpeek->fIsMRO & 1) != 0) projectorInfo = kModifiedReadOnly; else projectorInfo = kNotModifiable; // // Set the 'changed' bit if this item // is modifiable (we know that it is in a project) // if((projectorInfo & (kModifiedReadOnly | kCheckedOut)) != 0) projectorInfo |= kHasCKIDAndIsModifiable; if((projectorInfo & kCheckedOut) != 0) projectorInfo |= kHasCKIDAndIsCheckedOut; // // Hack: 'projectName' and fProjectName are // pascal strings, but they are always // null-terminated, so we can strcpy it // strcpy(projectName, &ckpeek->fProjectName); // // The string past the project name is the user name // (add one to skip the null termination) // p = &ckpeek->fProjectName + strlen(&ckpeek->fProjectName) + 1; // // The string past the user name is the revision number // (add one to skip the null termination) // Once again, we use strcpy, as the string is always // null-terminated. // p = p + strlen(p) + 1; strcpy(revisionNumber, p); HUnlock(ckidHandle); } CloseResFile(resRefNum); } //else // fprintf(stderr, "### Error opening resource fork of %P", fileName); return projectorInfo; } // ExamineProjectorInformation //---------------------------------------------------------------------------------------- // ProcessFile: //---------------------------------------------------------------------------------------- void ProcessFile(short vRefNum, long dirID, Str255 folderpath, Str255 filename, Boolean* didOutput) { char commaString[4]; char quoteString[4]; long fileProjectStatus = 0; // // Don't do a thing with this item if it's in the ignore list // if(ItemIsInIgnoreList(filename) == false) { // // Find out if the file has a 'ckid' resource. While we're at it, // extract information about the file (checked out, MRO'ed, etc, project // name, revision number) // fileProjectStatus = ExamineProjectorInformation(vRefNum, dirID, filename, gTempProjectName, gTempRevisionNumber); // // Ignore the file unless we want output for its state // if((gWantOutputForState & fileProjectStatus) != 0) { // // If doing short output (-s), only print the name of the file // if(OptionSpecified(kShortOutput)) { // // Print revision numbers if requested (-rev) // if((OptionSpecified(kOutputRevision)) && (gTempRevisionNumber[0] != 0)) { strcpy(commaString, ","); } else { commaString[0] = 0; gTempRevisionNumber[0] = 0; } // // Quote the string if necessary // if((OptionSpecified(kDontQuote) == false) && (HasSpecialCharacters(folderpath) || HasSpecialCharacters(filename))) strcpy(quoteString, "\'"); else quoteString[0] = 0; fprintf(stdout, "%s%P%P%s%P%s\n", quoteString, folderpath, filename, commaString, gTempRevisionNumber, quoteString); } else { // // At this point we are commiting to printing something; if we // have not printed anything yet, then add an extra blank line // so things look better // if(*didOutput == false) { fprintf(stdout, "\n"); } if(OptionSpecified(kOutputRevision)) sprintf(gTempRevisionString, "(%P,%P) ", filename, gTempRevisionNumber); else gTempRevisionString[0] = 0; switch(fileProjectStatus & kFileProjectMask) { case kNotInAProject: fprintf(stdout, "File \"%P%P\"; Line # is not in a project\n", folderpath, filename); break; case kNotModifiable: fprintf(stdout, "File \"%P%P\"; Line # %sis an unmodifiable copy of a file in the project %P\n", folderpath, filename, gTempRevisionString, gTempProjectName); break; case kModifiedReadOnly: fprintf(stdout, "File \"%P%P\"; Line # %sis a modify-read-only copy of a file in the project %P\n", folderpath, filename, gTempRevisionString, gTempProjectName); break; case kCheckedOut: fprintf(stdout, "File \"%P%P\"; Line # %sis a modifiable copy of a file in the project %P\n", folderpath, filename, gTempRevisionString, gTempProjectName); break; default: fprintf(stdout, "File \"%P%P\"; Line # %sis not recognized by Lurkers\n", folderpath, filename, gTempRevisionString); break; } // // didOutput actually means "printed a long line"; short lines don't // count. (This flag is used to determine if a trailing blank line // should be added to the output) // *didOutput = true; } } } } // ProcessFile //---------------------------------------------------------------------------------------- // Lurkers: //---------------------------------------------------------------------------------------- void Lurkers(short vRefNum, long dirID, long startLurkingDirID) { OSErr err = noErr; short index = 1; Boolean didOutput = false; // // Build the pathname to this folder if full paths were // specified or if we're not doing short output // gFolderpath[0] = 0; if(OptionSpecified(kFullPathnames) || OptionSpecified(kShortOutput)) BuildFolderPathname(vRefNum, dirID, gFolderpath, OptionSpecified(kShortOutput) && !OptionSpecified(kFullPathnames) ? startLurkingDirID : kMagicUnusedDirID, true); // // If doing long output, tell the user what we're doing // if(OptionSpecified(kShortOutput) == false) { fprintf(stdout, "### Scanning folder \"%P\"\n", gFolderpath); fflush(stdout); } // // Get rid of the pathname if we're not going to output it. // //if(OptionSpecified(kFullPathnames) == false) // gFolderpath[0] = 0; // // Walk every file in this directory // while(err == noErr) { SpinCursor(1); gFilename[0] = 0; pb.hFileInfo.ioCompletion = nil; pb.hFileInfo.ioNamePtr = gFilename; pb.hFileInfo.ioResult = noErr; pb.hFileInfo.ioVRefNum = vRefNum; pb.hFileInfo.ioDirID = dirID; pb.hFileInfo.ioFDirIndex = index; err = PBGetCatInfo(&pb,false); // // We only care about files right now... // if((err == noErr) && ((pb.hFileInfo.ioFlAttrib & (1 << 4)) == 0)) { // // If 'kTextOnly' is set, only process the file if its type // is 'TEXT'. Otherwise, always process the file // if( (OptionSpecified(kTextOnly) == false) || (pb.hFileInfo.ioFlFndrInfo.fdType == 'TEXT')) ProcessFile(vRefNum, dirID, gFolderpath, gFilename, &didOutput); } ++index; } // // Add another blank line if there was any output, // then flush stdout so that the text is actually // printed // if(didOutput) { fprintf(stdout, "\n"); } fflush(stdout); // // Do we want to do a deep search? // if(OptionSpecified(kRecursive)) { index = 1; err = noErr; // // Walk every folder in this directory // while(err == noErr) { gFilename[0] = 0; pb.dirInfo.ioCompletion = nil; pb.dirInfo.ioNamePtr = gFilename; pb.dirInfo.ioResult = noErr; pb.dirInfo.ioVRefNum = vRefNum; pb.dirInfo.ioDrDirID = dirID; pb.dirInfo.ioFDirIndex = index; err = PBGetCatInfo(&pb,false); // // Call Lurkers again on every folder we find... // if((err == noErr) && ((pb.hFileInfo.ioFlAttrib & (1 << 4)) != 0)) { // // Don't do the recursive call if this folder's name // appears in the ignore list // if(ItemIsInIgnoreList(gFilename) == false) { Lurkers(vRefNum, pb.dirInfo.ioDrDirID, startLurkingDirID); } } ++index; } } } // Lurkers //---------------------------------------------------------------------------------------- // main: //---------------------------------------------------------------------------------------- main( int argc, char* argv[] ) { Boolean optionsSpecified = false; Boolean didLurkers = false; short vRefNum = 0; long dirID = 0; short parms; short status = 0; Boolean* processedParameters; OSErr err = noErr; InitCursorCtl(nil); // // Set up the ignore list and the set of flags that indicates which // parameters we've processed. Both arrays only need to be 'argc' // elements in size, or smaller. // processedParameters = (Boolean*)NewPtr(sizeof(Boolean) * (argc + 1)); gIgnoreList = (char**)NewPtr(sizeof(Ptr) * (argc + 1)); for( parms = 0; parms < argc; ++parms ) { processedParameters[parms] = false; gIgnoreList[parms] = nil; } // // Run through the parameters once looking for things that start with "-" // for( parms = 1; parms < argc; parms++ ) { short length = strlen(argv[parms]); // // Look at all of the parameters that have a dash // if( argv[parms][0] == '-') { processedParameters[parms] = true; // // Process the specific flags // if( strcmp( argv[parms], "-modifiable" ) == 0 ) { optionsSpecified = true; gWantOutputForState = kModifiedReadOnly | kCheckedOut | kNotInAProject; } else if( strcmp( argv[parms], "-notmodifiable" ) == 0 ) { optionsSpecified = true; gWantOutputForState = kNotModifiable; } else if( strcmp( argv[parms], "-mro" ) == 0 ) { optionsSpecified = true; gWantOutputForState = kModifiedReadOnly; } else if( strcmp( argv[parms], "-notinanyproject" ) == 0 ) { optionsSpecified = true; gWantOutputForState = kNotInAProject; } else if( strcmp( argv[parms], "-insomeproject" ) == 0 ) { optionsSpecified = true; gWantOutputForState = kModifiedReadOnly | kCheckedOut | kNotModifiable; } else if( strcmp( argv[parms], "-modifiableinproject" ) == 0 ) { optionsSpecified = true; gWantOutputForState = kHasCKIDAndIsModifiable; } else if( strcmp( argv[parms], "-checkedoutmodifiable" ) == 0 ) { optionsSpecified = true; gWantOutputForState = kHasCKIDAndIsCheckedOut; } else if( strcmp( argv[parms], "-r" ) == 0 ) { optionsSpecified = true; gOptions |= kRecursive; } else if( strcmp( argv[parms], "-s" ) == 0 ) { optionsSpecified = true; gOptions |= kShortOutput; } else if( strcmp( argv[parms], "-f" ) == 0 ) { optionsSpecified = true; gOptions |= kFullPathnames; } else if( strcmp( argv[parms], "-q" ) == 0 ) { optionsSpecified = true; gOptions |= kDontQuote; } else if( strcmp( argv[parms], "-rev" ) == 0 ) { optionsSpecified = true; gOptions |= kOutputRevision; } else if( strcmp( argv[parms], "-textonly" ) == 0 ) { optionsSpecified = true; gOptions |= kTextOnly; } else if( strcmp( argv[parms], "-ignore" ) == 0 ) { optionsSpecified = true; ++parms; if(parms >= argc) { fprintf(stderr,"### %s - Encountered -ignore without a parameter", argv[0]); } else { processedParameters[parms] = true; gIgnoreList[gNumberOfIgnoreItems++] = argv[parms]; } } else { fprintf(stderr,"### %s - \"%s\" is not an option.\n", argv[0], argv[parms]); status = 1; break; } } } // // If neither -f nor -s, then -f // if((OptionSpecified(kFullPathnames) == false) && (OptionSpecified(kShortOutput) == false)) gOptions |= kFullPathnames; // // If all of the "-" parameters were processed okay, then run through // the parameters again looking for directory names // if(status == 0) { if((OptionSpecified(kShortOutput) == false)) { fprintf(stdout, "\n"); fflush(stdout); } for( parms = 1; parms < argc; parms++ ) { // // If we didn't process this parameter the first time through the // loop, then we assume it is the name of a directory search // or a file to test // if(processedParameters[parms] == false) { short length = strlen(argv[parms]); FSSpec tempFSSpec; strcpy(gFilename+1, argv[parms]); gFilename[0] = length; // // Make an FSSpec so that we can get the vRefNum of the specified // directory. // err = FSMakeFSSpec(0, 0, gFilename, &tempFSSpec); if(err == noErr) { vRefNum = tempFSSpec.vRefNum; // // Use PBGetCatInfo to get the dirID of the specified folder // pb.dirInfo.ioCompletion = nil; pb.dirInfo.ioNamePtr = &tempFSSpec.name; pb.dirInfo.ioResult = noErr; pb.dirInfo.ioVRefNum = vRefNum; pb.dirInfo.ioDrDirID = tempFSSpec.parID; pb.dirInfo.ioFDirIndex = 0; err = PBGetCatInfo(&pb,false); } // // Run Lurkers as soon as we get a directory or filename // (this is an easy and sleazy way to support multiple // folders/files on the command line) // if(err == noErr) { // // Is this a file or a folder? // if((pb.hFileInfo.ioFlAttrib & (1 << 4)) != 0) { dirID = pb.dirInfo.ioDrDirID; if(ItemIsInIgnoreList(tempFSSpec.name) == false) { Lurkers(vRefNum, dirID, dirID); } } else { Boolean unusedDidOutput; dirID = pb.hFileInfo.ioFlParID; // // If 'kTextOnly' is set, only process the file if its type // is 'TEXT'. Otherwise, always process the file // if( (OptionSpecified(kTextOnly) == false) || (pb.hFileInfo.ioFlFndrInfo.fdType == 'TEXT')) { // // At this point, 'gFileName' contains the file exactly as // the user specified it. Copy the filename from the temporary // FSSpec, which contains only the name. // Size len = tempFSSpec.name[0] + 1; BlockMove((Ptr)tempFSSpec.name, (Ptr)gFilename, len); // // Build the pathname to this folder if full paths were specified // gFolderpath[0] = 0; if(OptionSpecified(kFullPathnames)) { BuildFolderPathname(vRefNum, dirID, gFolderpath, kMagicUnusedDirID, true); } // fprintf(stderr, "### About to process <%P> in folder <%P> dirID <%ld>\n", gFilename, gFolderpath, dirID); ProcessFile(vRefNum, dirID, gFolderpath, gFilename, &unusedDidOutput); } } // // Make a note of the fact that we ran lurkers // didLurkers = true; } else { // // Set 'didLurkers' if we tried to run lurkers, too // fprintf(stderr, "### Error %d accessing %P\n", err, gFilename); didLurkers = true; } } } } // // if there were errors in the parameters, print usage // if((status == 1) || (didLurkers == false)) { fprintf(stderr, usage, argv[0]); // // Even more usage information for Andy... // fprintf(stderr, "\t©1994 Apple Computer, Inc. by Greg Anderson\n"); fprintf(stderr, "\n"); fprintf(stderr, "\tFile's projector status:\n"); fprintf(stderr, "\t\t-insomeproject\t\t\t# File is checked in to some project (has ckid)\n"); fprintf(stderr, "\t\t-notinanyproject\t\t# File is not checked in to any project (no ckid)\n"); fprintf(stderr, "\t\t-checkedoutmodifiable\t# File is checked in to some project and is\n"); fprintf(stderr, "\t\t\t\t\t\t\t\t#\tcurrently checked out for modification (but not MRO'ed)\n"); fprintf(stderr, "\t\t-modifiableinproject\t# Synonym for -checkedoutmodifiable\n"); fprintf(stderr, "\t\t-modifiable\t\t\t\t# Not in a project, in a project and checked out for modification,\n"); fprintf(stderr, "\t\t\t\t\t\t\t\t#\tor in a project and checked out modified-read-only\n"); fprintf(stderr, "\t\t-notmodifiable\t\t\t# In a project and not modifiable\n"); fprintf(stderr, "\t\t-mro\t\t\t\t\t# In a project and modified-read-only\n"); fprintf(stderr, "\n"); fprintf(stderr, "\tOptions:\n"); fprintf(stderr, "\t\t-r\t\t\t\t\t\t# Recursive search\n"); fprintf(stderr, "\t\t-s\t\t\t\t\t\t# Short output\n"); fprintf(stderr, "\t\t-q\t\t\t\t\t\t# Never quote filenames\n"); fprintf(stderr, "\t\t-f\t\t\t\t\t\t# Output full pathnames\n"); fprintf(stderr, "\t\t-rev\t\t\t\t\t# Output file's projector revision number (if any)\n"); fprintf(stderr, "\t\t-textonly\t\t\t\t# Ignore files whose type is not TEXT\n"); fprintf(stderr, "\t\t-ignore n\t\t\t\t# Ignore files and folders whose name is \"n\"\n"); fprintf(stderr, "\n"); fprintf(stderr, "\tSpecifying a file tests only that file. Specifying a directory tests every\n"); fprintf(stderr, "\tfile in that directory (and every file anywhere in that hierarchy if -r is used).\n"); } return status; } // main